/*******************************************************************************
 * Copyright (c) 2014 Francesco Gabbrielli
 * 
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * 
 * Contributors:
 *     Francesco Gabbrielli - initial API and implementation
 ******************************************************************************/
package it.francescogabbrielli.android.solidalapp;

import com.google.api.client.extensions.android.http.AndroidHttp;
import com.google.api.client.googleapis.json.GoogleJsonError;
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.client.googleapis.services.AbstractGoogleClient;
import com.google.api.client.googleapis.services.AbstractGoogleClientRequest;
import com.google.api.client.googleapis.services.GoogleClientRequestInitializer;
import com.google.api.client.googleapis.services.json.AbstractGoogleJsonClient;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson.JacksonFactory;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;

import java.io.IOException;
import java.io.Serializable;

/**
 * <p>
 * Common utilities for working with Cloud Endpoints.
 * 
 * If you'd like to test using a locally-running version of your App Engine
 * backend (i.e. running on the Development App Server), you need to set
 * LOCAL_ANDROID_RUN to 'true'.
 * 
 * <p>
 * See the documentation at
 * http://developers.google.com/eclipse/docs/cloud_endpoints for more
 * information.
 * 
 * <p>
 * [NEW] Added code to convert a json model to and from a bundle and for
 * building endpoints quickly
 */
public class EndpointsUtils {

	/*
	 * TODO: Need to change this to 'true' if you're running your backend
	 * locally using the DevAppServer. See
	 * http://developers.google.com/eclipse/docs/cloud_endpoints for more
	 * information.
	 */
	protected static final boolean LOCAL_ANDROID_RUN = false;

	/*
	 * The root URL of where your DevAppServer is running when it's being
	 * accessed via the Android emulator (if you're running the DevAppServer
	 * locally). In this case, you're running behind Android's virtual router.
	 * See
	 * http://developer.android.com/tools/devices/emulator.html#networkaddresses
	 * for more information.
	 */
	protected static final String LOCAL_APP_ENGINE_SERVER_URL_FOR_ANDROID = "http://192.168.0.100:8888";
	
	/**
	 * Change this to true if it uses a final backend version fixed address
	 */
	protected static final boolean FINAL_ANDROID_RUN = true;
	
	/**
	 * Fixed address to use (for final versions)
	 */
	protected static final String FINAL_APP_ENGINE_SERVER_URL_FOR_ANDROID = "https://0-3-final-dot-fg-solidal-app.appspot.com";

	/**
	 * Updates the Google client builder to connect the appropriate server based
	 * on whether LOCAL_ANDROID_RUN is true or false.
	 * 
	 * @param builder
	 *            Google client builder
	 * @return same Google client builder
	 */
	public static <B extends AbstractGoogleClient.Builder> B updateBuilder(
			B builder) {
		if (LOCAL_ANDROID_RUN) {
			builder.setRootUrl(LOCAL_APP_ENGINE_SERVER_URL_FOR_ANDROID
					+ "/_ah/api/");
		} else if (FINAL_ANDROID_RUN) {
			builder.setRootUrl(FINAL_APP_ENGINE_SERVER_URL_FOR_ANDROID
					+ "/_ah/api/");
		}

		// only enable GZip when connecting to remote server
		final boolean enableGZip = builder.getRootUrl().startsWith("https:");

		builder.setGoogleClientRequestInitializer(new GoogleClientRequestInitializer() {
			public void initialize(AbstractGoogleClientRequest<?> request)
					throws IOException {
				if (!enableGZip) {
					request.setDisableGZipContent(true);
				}
			}
		});

		return builder;
	}

	/**
	 * Logs the given message and shows an error alert dialog with it.
	 * 
	 * @param activity
	 *            activity
	 * @param tag
	 *            log tag to use
	 * @param message
	 *            message to log and show or {@code null} for none
	 */
	public static void logAndShow(Activity activity, String tag, String message) {
		Log.e(tag, message);
		showError(activity, message);
	}

	/**
	 * Logs the given throwable and shows an error alert dialog with its
	 * message.
	 * 
	 * @param activity
	 *            activity
	 * @param tag
	 *            log tag to use
	 * @param t
	 *            throwable to log and show
	 */
	public static void logAndShow(Activity activity, String tag, Throwable t) {
		Log.e(tag, "Error", t);
		String message = t.getMessage();
		// Exceptions that occur in your Cloud Endpoint implementation classes
		// are wrapped as GoogleJsonResponseExceptions
		if (t instanceof GoogleJsonResponseException) {
			GoogleJsonError details = ((GoogleJsonResponseException) t)
					.getDetails();
			if (details != null) {
				message = details.getMessage();
			}
		}
		showError(activity, message);
	}

	/**
	 * Shows an error alert dialog with the given message.
	 * 
	 * @param activity
	 *            activity
	 * @param message
	 *            message to show or {@code null} for none
	 */
	public static void showError(final Activity activity, String message) {
		final String errorMessage = message == null ? "Error" : "[Error ] "
				+ message;
		activity.runOnUiThread(new Runnable() {
			public void run() {
				Toast.makeText(activity, errorMessage, Toast.LENGTH_LONG)
						.show();
			}
		});
	}

	/**
	 * Build an endpoint (quick and dirt)
	 * 
	 * @param name
	 * 		the class of the endpoint
	 * @return
	 * 		the endpoint
	 * @throws Exception
	 * 		of the kind if reflection fails
	 */
	public static <E extends AbstractGoogleJsonClient> E buildEndpoint(
			Class<E> epClass) throws Exception {

//		Class<?> c = null;
//		Class<?>[] inner = epClass.getDeclaredClasses();
//		Log.i(SolidalApp.TAG, "INNER: "+(inner!=null ? ""+inner.length : "null"));
		Class<?> bc = Class.forName(epClass.getCanonicalName()+"$Builder");
//		Log.v(SolidalApp.TAG, "BUILDER CLASS: "+bc);
//		for (Class<?> bc : inner)
//			if ("Builder".equals(bc.getSimpleName())) {
//				c = bc;
//				Log.i(SolidalApp.TAG, "BUILDER CLASS: "+c);
//				break;
//			}
		
		AbstractGoogleJsonClient.Builder builder = (AbstractGoogleJsonClient.Builder) bc
					.getConstructor(
						new Class[] { 
								HttpTransport.class, 
								JsonFactory.class,
								HttpRequestInitializer.class 
						})
					.newInstance(
						new Object[] { 
								AndroidHttp.newCompatibleTransport(),
								new JacksonFactory(),
								new HttpRequestInitializer() {
									@Override
									public void initialize(HttpRequest httpRequest) {
										httpRequest.setConnectTimeout(30000);
										httpRequest.setReadTimeout(60000);
									}
								} 
						});
		builder.setApplicationName(SolidalApp.TAG);
		EndpointsUtils.updateBuilder(builder);
		@SuppressWarnings("unchecked")
		E ret = (E) builder.build();
		return ret;

	}

	/**
	 * Make bundle from json model
	 * 
	 * @param model
	 * 		the model
	 * @return
	 * 		the bundle (empty if no field can be put in)
	 */
	public static Bundle toBundle(com.google.api.client.json.GenericJson model) {
		if (model == null)
			return null;
		Bundle ret = new Bundle();
		for (String key : model.keySet())
			try {
				Object val = model.get(key);
				if (val != null)
					if (val instanceof GenericJson) {
						ret.putParcelable(key, toBundle((GenericJson) val));
					} else {
						ret.putSerializable(key, (Serializable) val);
					}
			} catch (Exception e) {
				Log.w(SolidalApp.TAG,
						"Cannot wrap key " + key + "=" + model.get(key), e);
			}
		try {
			ret.putString("toString", model.toPrettyString());
		} catch (IOException e) {
			Log.w(SolidalApp.TAG, "Error formatting toString for: " + model);
		}
		return ret;
	}
	
	/**
	 * Make a JSON model from a bundle, guessing the class from the Key. Does work 
	 * only with models (and bundles obtained from such models) with app engine Key 
	 * fields
	 * 
	 * @param bundle
	 * 		the bundle
	 * @return
	 * 		the model
	 */
	public static <T extends com.google.api.client.json.GenericJson> T fromBundle(Bundle bundle) {
		try {
			for (String key : bundle.keySet()) {
				if (key.endsWith("Key") && bundle.get(key) instanceof Bundle) {
					Bundle keyBundle = (Bundle) bundle.get(key);
					String kind = (String) keyBundle.get("kind");
					String pkg = EndpointsUtils.class.getPackage().getName()
							+ "."+kind.toLowerCase()
							+ "endpoint.model";
					@SuppressWarnings("unchecked")
					Class<T> c = (Class<T>) Class.forName(pkg+"."+kind);
					return fromBundle(bundle, c);
				}
			}
		} catch(Exception e) {
			Log.e(SolidalApp.TAG, "Cannot make JSON from bundle: "+bundle, e);
		}
		return null;
	}

	/**
	 * Make a JSON model from a bundle
	 * 
	 * @param bundle
	 *		the bundle
	 * @param destClass
	 *		the model class
	 * @return 
	 * 		the model
	 */
	public static <T extends com.google.api.client.json.GenericJson> T fromBundle(
			Bundle bundle, Class<T> destClass) {
		if (bundle == null)
			return null;
		String name = destClass.getSimpleName();
		try {
			T ret = destClass.newInstance();
			for (String key : bundle.keySet()) {
				Object value = bundle.get(key);
				if (value != null)
					if (value instanceof Bundle) {
						@SuppressWarnings("unchecked")
						Class<? extends GenericJson> keyClass = (Class<? extends GenericJson>) destClass
								.getDeclaredField(key).getType();
						ret.set(key, fromBundle((Bundle) value, keyClass));
					} else {
						if (!"toString".equals(key))
							ret.set(key, value);
					}
			}
			return ret;
		} catch (InstantiationException e) {
			Log.w(SolidalApp.TAG, "Cannot make JSON from bundle: instantiate " + name, e);
		} catch (IllegalAccessException e) {
			Log.w(SolidalApp.TAG, "Cannot make JSON from bundle: access " + name, e);
		} catch (Exception e) {
			Log.w(SolidalApp.TAG, "Cannot make JSON from bundle: " + bundle, e);
		}
		return null;
	}
	
	public static String toString(Bundle extras) {
		StringBuilder ret = new StringBuilder("{");
		try {
			if (extras!=null)
				for (String key : extras.keySet())
					if (!"toString".equals(key))
						ret.append(key).append("=").append(extras.get(key)).append(";");
		} catch(Exception e) {}
		ret.append("}");
		return ret.toString();
	}

}
